package pers.cz.javacvdemo.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * @author yiyi.su
 * @version 1.0.0
 * @description RedisService
 * @createTime 2022/8/15
 */
@Slf4j
public class RedisService {

	public final HashOp HASH;
	public final ListOp LIST;
	public final StringOp STRING;
	public final SetOp SET;
	public final SortedSetOp ZSET;
	private final StringRedisTemplate redisTemplate;

	public RedisService(StringRedisTemplate redisTemplate) {
		this.redisTemplate = redisTemplate;
		this.HASH = new HashOp(redisTemplate);
		this.LIST = new ListOp(redisTemplate);
		this.STRING = new StringOp(redisTemplate);
		this.SET = new SetOp(redisTemplate);
		this.ZSET = new SortedSetOp(redisTemplate);
	}

	public Boolean del(String key) {
		try {
			return redisTemplate.delete(key);
		} catch (Exception e) {
			log.warn("del exception, key={}", key, e);
		}
		return false;
	}

	public Long del(Collection<String> keys) {
		try {
			return redisTemplate.delete(keys);
		} catch (Exception e) {
			log.warn("del exception, keys={}", keys, e);
		}
		return null;
	}

	public Boolean exists(String key) {
		try {
			return redisTemplate.hasKey(key);
		} catch (Exception e) {
			log.warn("exists exception, key={}", key, e);
		}
		return null;
	}

	public Boolean expire(String key, long timeout, TimeUnit unit) {
		try {
			return redisTemplate.expire(key, timeout, unit);
		} catch (Exception e) {
			log.warn("expire exception, key={},timeout={},unit={}", key, timeout, unit, e);
		}
		return null;
	}

	public Boolean expire(String key, Duration timeout) {
		try {
			return redisTemplate.expire(key, timeout);
		} catch (Exception e) {
			log.warn("expire exception, key={},timeout={}", key, timeout, e);
		}
		return null;
	}

	public Boolean expireAt(String key, Date date) {
		try {
			return redisTemplate.expireAt(key, date);
		} catch (Exception e) {
			log.warn("expireAt exception, key={},date={}", key, date, e);
		}
		return null;
	}

	public Boolean persist(String key) {
		try {
			return redisTemplate.persist(key);
		} catch (Exception e) {
			log.warn("persist exception, key={}", key, e);
		}
		return null;
	}

	public Long pTTL(String key) {
		try {
			return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
		} catch (Exception e) {
			log.warn("pTTL exception, key={}", key, e);
		}
		return null;
	}

	public Long ttl(String key) {
		try {
			return redisTemplate.getExpire(key);
		} catch (Exception e) {
			log.warn("ttl exception, key={}", key, e);
		}
		return null;
	}

	public String randomKey() {
		try {
			return redisTemplate.randomKey();
		} catch (Exception e) {
			log.warn("randomKey exception", e);
		}
		return null;
	}

	public void rename(String oldKey, String newKey) {
		try {
			redisTemplate.rename(oldKey, newKey);
		} catch (Exception e) {
			log.warn("rename exception, oldKey={},newKey={}", oldKey, newKey, e);
		}
	}

	public void renameNX(String oldKey, String newKey) {
		try {
			redisTemplate.renameIfAbsent(oldKey, newKey);
		} catch (Exception e) {
			log.warn("renameNX exception, oldKey={},newKey={}", oldKey, newKey, e);
		}
	}

	public DataType type(String key) {
		try {
			return redisTemplate.type(key);
		} catch (Exception e) {
			log.warn("type exception, key={}", key, e);
		}
		return null;
	}

	public Cursor<String> scan(ScanOptions options) {
		try {
			return redisTemplate.scan(options);
		} catch (Exception e) {
			log.warn("scan exception, options={}", options, e);
		}
		return null;
	}

	public <T> T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) {
		return redisTemplate.execute((RedisCallback<T>) connection -> connection.scriptingCommands().eval(script, returnType, numKeys, keysAndArgs));
	}

	public String wrapperLockKey(String key) {
		return "lock:" + key;
	}

	private final byte[] UNLOCK_SCRIPT = """
			if redis.call('get',KEYS[1]) == ARGV[1] then
			   return redis.call('del',KEYS[1])
			else
			   return 0
			end;
			""".getBytes(StandardCharsets.UTF_8);

	public boolean lock(String lockKey, String lockValue, int expirationSecond) {
		while (!STRING.set(wrapperLockKey(lockKey), lockValue, Expiration.seconds(expirationSecond), RedisStringCommands.SetOption.SET_IF_ABSENT)) {
			log.debug("wait redis lock: lockKey={},", lockKey);
			try {
				TimeUnit.MILLISECONDS.sleep(100);
			} catch (InterruptedException e) {
				log.warn("wait redis lock exception.", e);
				return false;
			}
		}
		return true;
	}

	public boolean unlock(String lockKey, String lockValue) {
		byte[][] keyAndArgs = {STRING.rawKey(wrapperLockKey(lockKey)), STRING.rawValue(STRING.serialize(lockValue))};
		Long result = eval(UNLOCK_SCRIPT, ReturnType.INTEGER, 1, keyAndArgs);
		return result != null && result > 0L;
	}

	public <T> GetAndSetBuilder<T> getAndSet(String key) {
		return new GetAndSetBuilder<>(this, key);
	}

	public static class GetAndSetBuilder<T> {

		private final RedisService redisService;
		private final String key;

		public GetAndSetBuilder(RedisService redisService, String key) {
			this.redisService = redisService;
			this.key = key;
		}

		public GetBuilder<T> get(Supplier<T> get) {
			return new GetBuilder<>(redisService, key, get);
		}

	}

	public static class GetBuilder<T> {

		private final RedisService redisService;
		private final String key;
		private final Supplier<T> get;

		public GetBuilder(RedisService redisService, String key, Supplier<T> get) {
			this.redisService = redisService;
			this.key = key;
			this.get = get;
		}

		public SetBuilder<T> onMiss(Predicate<T> predicate) {
			return new SetBuilder<>(redisService, key, get, predicate);
		}
	}

	public static class SetBuilder<T> {

		private final RedisService redisService;
		private final String key;

		private final Supplier<T> get;

		private final Predicate<T> predicate;

		public SetBuilder(RedisService redisService, String key, Supplier<T> get, Predicate<T> predicate) {
			this.redisService = redisService;
			this.key = key;
			this.get = get;
			this.predicate = predicate;
		}

		public T execute(Supplier<T> db) {
			T object = get.get();
			if (!predicate.test(object)) {
				return object;
			}
			String lockKey = "getAndSet:" + key;
			String value = UUID.randomUUID().toString();
			try {
				if (redisService.lock(lockKey, value, 10)) {
					object = get.get();
					if (!predicate.test(object)) {
						return object;
					}
					return db.get();
				}
			} finally {
				if (!redisService.unlock(lockKey, value)) {
					log.warn("unlock error. key={}", lockKey);
				}
			}
			return null;
		}
	}
}
